/*
 *@Type NormalStore.java
 * @Desc
 * @Author urmsone urmsone@163.com
 * @date 2024/6/13 02:07
 * @version
 */
package service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import model.command.Command;
import model.command.CommandPos;
import model.command.RmCommand;
import model.command.SetCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utils.CommandUtil;
import utils.LoggerUtil;
import utils.RandomAccessFileUtil;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class NormalStore implements Store {

    public static final String TABLE = ".table";
    public static final String RW_MODE = "rw";
    public static final String NAME = "data";
    private final Logger LOGGER = LoggerFactory.getLogger(NormalStore.class);
    private final String logFormat = "[NormalStore][{}]: {}";
    private static final int maxStoreFileSize = 50; // 50B(最大可存储容量)
//    private static final int maxStoreFileSize = 1024 * 10 - 50; // 1024 * 10 - 50KB(最大可存储容量)

    private int NUM = 1;


    /**
     * 内存表，类似缓存
     */
    private TreeMap<String, Command> memTable;

    /**
     * hash索引，存的是数据长度和偏移量
     */
    private HashMap<String, CommandPos> index;

    /**
     * 数据目录
     */
    private final String dataDir;

    /**
     * 读写锁，支持多线程，并发安全写入
     */
    private final ReadWriteLock indexLock;

    /**
     * 暂存数据的日志句柄
     */
    private RandomAccessFile writerReader;

    /**
     * 持久化阈值
     */
    private final int storeThreshold;

    public NormalStore(String dataDir) {
        this.dataDir = dataDir;
        this.indexLock = new ReentrantReadWriteLock();
        this.memTable = new TreeMap<String, Command>();
        this.index = new HashMap<>();
        this.storeThreshold = 3; // 设置阀值

        File file = new File(dataDir);
        if (!file.exists()) {
            LoggerUtil.info(LOGGER, logFormat, "NormalStore", "dataDir isn't exist,creating...");
            file.mkdirs();
        }
        // 检查日志是否为空，不为空则上次操作发生异常，将里面的内容写进数据文件
        reloadLog();

        this.reloadIndex();

        // 注册JVM关闭钩子,确保在JVM退出时将内存中的数据刷新到磁盘
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            try {

//                System.exit(1);
                flushMemTableToDisk();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }));
    }

    public String genFilePath() {
        return this.dataDir + File.separator + NAME + NUM + TABLE;
    }

    /**
     * 重新加载索引。
     * 此方法从指定的文件中读取命令数据,并构建命令索引。
     * 索引中包含了每个命令的键以及其在文件中的位置和长度信息。
     */
    public void reloadIndex() {

        try {

            NUM = 1;

            while (true) {
                // 判断该文件是否存在，不存在就切换为上一个文件
                File files = new File(genFilePath());
                if(!files.exists()) {
                    if(NUM == 1) return;

                    else {
                        NUM -= 1;
                        return;
                    }

                }

                // 以读写模式打开文件
                RandomAccessFile file = new RandomAccessFile(genFilePath(), RW_MODE);

                // 获取文件长度
                long len = file.length();
                // 从文件开头开始读取
                long start = 0;
                file.seek(start);

                // 逐个读取文件中的命令
                while (start < len) {
                    // 读取命令长度
                    int cmdLen = file.readInt();
                    // 创建字节数组读取命令内容
                    byte[] bytes = new byte[cmdLen];
                    file.read(bytes);
                    // 将字节数组解析为JSON对象
                    JSONObject value = JSON.parseObject(new String(bytes, StandardCharsets.UTF_8));
                    // 将JSON对象转换为Command对象
                    Command command = CommandUtil.jsonToCommand(value);
                    // 记录当前命令在文件中的起始位置
                    start += 4;
                    // 如果成功解析了命令
                    if (command != null) {
                        // 创建CommandPos对象记录命令的位置和长度
                        CommandPos cmdPos = new CommandPos((int) start, cmdLen,NUM);
                        // 将命令的键和其位置信息添加到索引中
                        index.put(command.getKey(), cmdPos);
                    }
                    // 移动到下一个命令的起始位置
                    start += cmdLen;
                }

                NUM += 1;
            }

        } catch (Exception e) {
            // 捕获任何异常并打印栈trace
            e.printStackTrace();
        }

        // 记录日志,输出重新加载索引的结果
        LoggerUtil.debug(LOGGER, logFormat, "reload index: " + index.toString());
    }

    @Override
    public void set(String key, String value) {
        try {
            SetCommand command = new SetCommand(key, value);
            byte[] commandBytes = JSONObject.toJSONBytes(command);
            // 加锁
            indexLock.writeLock().lock();

            // TODO://先写内存表，内存表达到一定阀值再写进磁盘
//            // 写table（log）文件
            RandomAccessFileUtil.writeInt("log/log.wal", commandBytes.length);
            int pos = RandomAccessFileUtil.write("log/log.wal", commandBytes);
            // 保存到memTable
            memTable.put(key, command);
            // 添加索引
            CommandPos cmdPos = new CommandPos(pos, commandBytes.length,NUM);
            index.put(key, cmdPos);

            // TODO://判断是否需要将内存表中的值写回table
            if (memTable.size() >= storeThreshold) {
                flushMemTableToDisk();
            }
        } catch (Throwable t) {
            throw new RuntimeException(t);
        } finally {
            indexLock.writeLock().unlock();
        }
    }

    @Override
    public String get(String key) {
        try {

            indexLock.readLock().lock();
//            // 先从内存表获取
            if (memTable.containsKey(key)) {
                Command cmd = memTable.get(key);
                if (cmd instanceof SetCommand) {
                    return ((SetCommand) cmd).getValue();
                }
                if (cmd instanceof RmCommand) {
                    return null;
                }
            }
            // 从索引中获取信息
            CommandPos cmdPos = index.get(key);
            if (cmdPos == null) {
                return null;
            }
            byte[] commandBytes = RandomAccessFileUtil.readByIndex("data/data" + cmdPos.getPositionNum() + TABLE, cmdPos.getPos(), cmdPos.getLen());

            // 将命令字节解析为JSON对象,并将其转换为Command对象
            JSONObject value = JSONObject.parseObject(new String(commandBytes));

            // 根据Command对象的类型,返回适当的值
            Command cmd = CommandUtil.jsonToCommand(value);
            if (cmd instanceof SetCommand) {
                return ((SetCommand) cmd).getValue();
            }
            if (cmd instanceof RmCommand) {
                return null;
            }

        } catch (Throwable t) {
            throw new RuntimeException(t);
        } finally {
            indexLock.readLock().unlock();
        }
        return null;
    }

    @Override
    public void rm(String key) {
        try {
            RmCommand command = new RmCommand(key);
            // 将 Java 对象 command 序列化为 JSON 格式的字节数组
            byte[] commandBytes = JSONObject.toJSONBytes(command);
            // 加锁
            indexLock.writeLock().lock();
            // TODO://先写内存表，内存表达到一定阀值再写进磁盘
            // 写table（wal）文件
            RandomAccessFileUtil.writeInt("log/log.wal", commandBytes.length);
            int pos = RandomAccessFileUtil.write("log/log.wal", commandBytes);
            // 保存到memTable
            memTable.put(key, command);
            // 添加索引
            CommandPos cmdPos = new CommandPos(pos, commandBytes.length,NUM);
            index.put(key, cmdPos);

            // TODO://判断是否需要将内存表中的值写回table
            if (memTable.size() >= storeThreshold) {
                flushMemTableToDisk();
            }

        } catch (Throwable t) {
            throw new RuntimeException(t);
        } finally {
            indexLock.writeLock().unlock();
        }
    }

    private void flushMemTableToDisk() throws IOException {
        try {

            // 遍历memTable中的所有键
            for (String key : memTable.keySet()) {
                // 获取与当前键关联的Command对象
                Command command = memTable.get(key);

                // 使用JSON序列化将Command对象转换为字节数组
                byte[] commandBytes = JSONObject.toJSONBytes(command);

                // 将命令字节的长度写入文件
                RandomAccessFileUtil.writeInt(this.genFilePath(), commandBytes.length);

                // 将命令字节写入文件,并获取写入的位置
                RandomAccessFileUtil.write(this.genFilePath(), commandBytes);
            }

            // 将memTable的内容刷新到磁盘后,清空memTable
            memTable.clear();

            // 清空log日志文件内容
            clearInfoForFile("log/log.wal");

            File files = new File(this.genFilePath());

            //如果磁盘文件大小大于maxStoreFileSize，则进行压缩处理
            if (files.length() >= maxStoreFileSize) {
                Thread t = new MyThread();
                // 启动线程（自动执行run方法的）
                t.start();
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                index.clear();
                reloadIndex();
            }

        } catch (RuntimeException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void close() throws IOException {
        flushMemTableToDisk();
    }

    /**
     * 将log文件里面的内容写入内存表
     */
    public void reloadLog() {
        try {

            // 以读写模式打开文件
            RandomAccessFile file = new RandomAccessFile("log/log.wal", RW_MODE);
            // 获取文件长度
            long len = file.length();
            // 从文件开头开始读取
            long start = 0;
            file.seek(start);

            if (len == 0) return;
            else System.out.println("程序异常退出，正执行回放操作...");

            // 逐个读取文件中的命令
            while (start < len) {
                // 读取命令长度
                int cmdLen = file.readInt();

                // 创建字节数组读取命令内容
                byte[] bytes = new byte[cmdLen];
                file.read(bytes);
                // 将字节数组解析为JSON对象
                JSONObject value = JSON.parseObject(new String(bytes, StandardCharsets.UTF_8));
                // 将JSON对象转换为Command对象
                Command command = CommandUtil.jsonToCommand(value);

                // 记录当前命令在文件中的起始位置
                start += 4;
                // 如果成功解析了命令
                if (command != null) {
                    // 保存到memTable
                    memTable.put(command.getKey(), command);
                    // 创建CommandPos对象记录命令的位置和长度和在哪一个文件
                    CommandPos cmdPos = new CommandPos((int) start, cmdLen,0);
                    // 将命令的键和其位置信息添加到索引中
                    index.put(command.getKey(), cmdPos);
                }
                // 移动到下一个命令的起始位置
                start += cmdLen;
            }
            // 将文件指针移动到文件末尾
            file.seek(file.length());

            // 将日志文件写入内存表之后再写入数据文件
            flushMemTableToDisk();

        } catch (Exception e) {
            // 捕获任何异常并打印栈trace
            e.printStackTrace();
        }
        // 记录日志,输出重新加载索引的结果
        LoggerUtil.debug(LOGGER, logFormat, "reload index: " + index.toString());
    }

    /**
     * 清空文件内容
     */
    public void clearInfoForFile(String fileName) {
        File file = new File(fileName);
        try {
            if (!file.exists()) {
                file.createNewFile();
            }
            FileWriter fileWriter = new FileWriter(file);
            fileWriter.write("");
            fileWriter.flush();
            fileWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
